﻿//*** Guidance generated code ***//

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Http.ValueProviders;
using System.Web.Http.ValueProviders.Providers;
using $prjsn$.RestApi;
using ProjectBase.Core;

namespace $safeprojectname$.RestApi
{
    public class ODataParser
    {
        #region members
        protected const string ODfilter = "$filter";
        protected const string ODorderby = "$orderby";
        protected const string ODtop = "$top";
        protected const string ODselect = "$select";
        protected const string ODskip = "$skip";
        protected const string ODinlineCount = "$inlinecount";
        protected const string ODexpand = "$expand";

        protected const string AppendFrom = "From";
        protected const string AppendTo = "To";

        protected const string GeneralError = "Wrong format of the ODATA string";
        protected const string Ascending = "asc";

        protected readonly IList<string> IDToChange = new[] { "ID", "IdExact" };
        protected readonly IList<string> Delimiters = new[] { " lt ", " gt ", " le ", " ge ", " eq ", " ne " };
        protected readonly IList<string> GlobalDelimiters = new[] { " and ", " or ", " not " };
        protected readonly IList<string> GreaterDelimiters = new[] { " gt ", " ge " };
        protected readonly IList<string> LesserDelimiters = new[] { " lt ", " le " };
        protected readonly IList<string> OrderByParams = new[] { " asc", " desc" };


        //Specify the filter methods we are able to work with
        protected readonly IList<string> FunctionNames = new[] { "startswith", "endswith", "substringof" };

        #endregion members

        public IValueProvider Parse(string odataQueryString, ref ServiceMessage message)
        {
            //Messaging
            message = new ServiceMessage();

            #region delimiters declaration
            string[] splitByQuestionMark = odataQueryString.Split('?');
            string queryString = splitByQuestionMark.Length > 1 ? splitByQuestionMark[1] : string.Empty;

            var commaSeparator = new Regex(",");
            #endregion

            queryString = Regex.Replace(queryString, "/", ".");

            NameValueCollection queryStringCollection = HttpUtility.ParseQueryString(queryString);
            if (queryStringCollection.AllKeys.Length == 0)
            {
                //message.Add(Severity.Error, GeneralError);
                return null;
            }

            var localCollection = new NameValueCollection();

            foreach (var key in queryStringCollection.AllKeys)
            {
                if (key.IsEmpty())
                {
                    continue; // it could be different Custome part than OData $filter, $top...
                }

                var values = queryStringCollection.GetValues(key);

                var isOptionParsed
                    = ParseSelect(key, values, localCollection, message)
                    || ParseExpand(key, values, localCollection, message)
                    || ParseOrderBy(key, values, localCollection, message)
                    || ParseNumbers(key, values, localCollection, message, ODtop)
                    || ParseNumbers(key, values, localCollection, message, ODskip)
                    || ParseInlineCount(key, values, localCollection, message)
                    || ParseFilter(key, localCollection, queryStringCollection[key], message)
                    ;

                // one of OData options was found 
                // and Messages contains error
                if (isOptionParsed)
                {
                    continue;
                }

                if (key.StartsWith("$"))
                {
                    message.Add(Severity.Error, "Not supported ODATA query option {0} ".FormatWith(key));
                }
            }

            if (message.Messages.Count > 0)
            {
                return null;
            }

            //Now lets transfer the parsed ODATA string to the _collection from localCollection. This could be done earlier on flight, but I think keeping the initial parsing could be useful in case in future we
            //would like to change conversion policy e.t.c.
            NameValueCollection collection = OdataCollectionToInternalCollection(localCollection);

            IValueProvider provider = ToProvider(collection);

            return provider;
        }

        #region parse OData Options
        /// <summary>returns true if key.IsEqual($filter). </summary>
        protected virtual bool ParseFilter(string key, NameValueCollection localCollection, string queryStringValue, ServiceMessage message)
        {
            if (!key.IsEqual(ODfilter))
            {
                return false; //Only proceed $select and its parameters here
            }

            if (queryStringValue.IsEmpty())
            {
                return true;
            }

            string globalDelimitersPattern = string.Join("|", GlobalDelimiters);
            string delimitersPattern = string.Join("|", Delimiters);

            var functionStorage = new string[2];
            functionStorage[0] = null;
            functionStorage[1] = null;

            string[] substrings = Regex.Split(queryStringValue, globalDelimitersPattern + "|,", RegexOptions.IgnoreCase); //separate raw string using global delimiters and comma

            for (int j = 0; j < substrings.Length; j++)
            {
                if (GlobalDelimiters.Any(s => substrings[j].ToLower().Contains(s)))
                {
                    string[] preFinalSubstrings = Regex.Split(substrings[j], globalDelimitersPattern, RegexOptions.IgnoreCase);
                    for (int c = 0; c < preFinalSubstrings.Length; c++)
                    {
                        ResolveComparison(preFinalSubstrings, c, delimitersPattern, localCollection, ref message);
                        if (message != null)
                        {
                            return true;
                        }
                    }
                }
                else if (Delimiters.Any(s => substrings[j].ToLower().Contains(s))) //Most simple case, no global delimiters, just right and left operands
                {
                    string append = null;

                    if (GreaterDelimiters.Any((s => substrings[j].ToLower().Contains(s))))
                    {
                        append = AppendFrom;
                    }
                    else if (LesserDelimiters.Any((s => substrings[j].ToLower().Contains(s))))
                    {
                        append = AppendTo;
                    }

                    string[] finalSubstrings = Regex.Split(substrings[j], delimitersPattern, RegexOptions.IgnoreCase);
                    localCollection.Add(finalSubstrings[0].Substring(finalSubstrings[0].IndexOf('?') + 1).Trim() + append, finalSubstrings[1]); //We are done with the substring by this
                }
                else //handle filter methods in here
                {
                    #region filter methods
                    //Filter method syntax is wrong
                    if (substrings[j] == string.Empty)
                    {
                        //prepare name of the filter method for the error message
                        string filterMethod = null;
                        if (functionStorage[0] != null && functionStorage[0] != string.Empty)
                        {
                            int index = functionStorage[0].LastIndexOf("(");
                            filterMethod = functionStorage[0].Substring(0, index);
                        }

                        message.Add(Severity.Error, "Odata query string contains an error. Incorrect syntax in filter method " + filterMethod);
                        return true;
                    }

                    //So far filter method syntax is correct, proceed...
                    if (functionStorage[0] == null)
                    {
                        functionStorage[0] = substrings[j];
                    }
                    else if (functionStorage[1] == null)
                    {
                        functionStorage[1] = substrings[j];

                        //Get rid of string artefacts we don't need
                        functionStorage[1] = functionStorage[1].Trim().Trim(')').Trim().Trim('\'').Trim();

                        if (FunctionNames[0].ToLower().All(s => functionStorage[0].ToLower().Contains(s))) //startswith
                        {
                            functionStorage[0] = functionStorage[0].Substring(functionStorage[0].IndexOf('(') + 1).Trim().Trim('\'').Trim(); //Get rid of string artefacts we don't need
                            localCollection.Add(functionStorage[0], functionStorage[1] + "*");
                        }
                        else if (FunctionNames[1].ToLower().All(s => functionStorage[0].ToLower().Contains(s))) //endswith
                        {
                            functionStorage[0] = functionStorage[0].Substring(functionStorage[0].IndexOf('(') + 1).Trim().Trim('\'').Trim(); //Get rid of string artefacts we don't need
                            localCollection.Add(functionStorage[0], "*" + functionStorage[1]);
                        }
                        else if (FunctionNames[2].ToLower().All(s => functionStorage[0].ToLower().Contains(s))) //substringof
                        {
                            functionStorage[0] = functionStorage[0].Substring(functionStorage[0].IndexOf('(') + 1).Trim().Trim('\'').Trim(); //Get rid of string artefacts we don't need
                            localCollection.Add(functionStorage[1], "*" + functionStorage[0] + "*");
                        }

                        functionStorage[0] = functionStorage[1] = null; //Filter method proceeded, get ready for the next one
                    }
                    #endregion
                } //end methods
            }
            return true;
        }

        /// <summary>returns true if key.IsEqual(odataOption). </summary>
        protected virtual bool ParseNumbers(string key, IEnumerable<string> values
            , NameValueCollection localCollection, ServiceMessage message, string odataOption)
        {
            if (!key.IsEqual(odataOption))
            {
                return false; //Only proceed $top or $skip and its parameters here
            }

            if (values.IsEmpty())
            {
                return true;
            }

            if (values.Count() > 1)
            {
                message.Add(Severity.Error, "ODATA query option {0} was specified more than once".FormatWith(odataOption));
                return true;
            }

            var value = values.First().Trim();

            int number;
            if (!int.TryParse(value, out number))
            {
                message.Add(Severity.Error, "ODATA query option {0} contains non-integer value".FormatWith(odataOption));
                return true;
            }
            localCollection.Add(key, value);
            return true;
        }
        /// <summary>returns true if key.IsEqual($expand). </summary>
        protected virtual bool ParseExpand(string key, IEnumerable<string> values
           , NameValueCollection localCollection, ServiceMessage message)
        {
            if (!key.IsEqual(ODexpand))
            {
                return false; //Only proceed $expand and its parameters here
            }
            return true;
        }
        /// <summary>returns true if key.IsEqual($inlinecount). </summary>
        protected virtual bool ParseInlineCount(string key, IEnumerable<string> values
            , NameValueCollection localCollection, ServiceMessage message)
        {
            if (!key.IsEqual(ODinlineCount))
            {
                return false; //Only proceed $inlinecount and its parameters here
            }

            if (values.IsEmpty())
            {
                return true;
            }

            if (values.Count() > 1)
            {
                message.Add(Severity.Error, "ODATA query option {0} was specified more than once".FormatWith(ODinlineCount));
                return true;
            }

            var value = values.First().Trim();
            if (value.IsEqual("allpages"))
            {
                localCollection.Add(key, "true");
            }
            else if (value.IsEqual("none"))
            {
                localCollection.Add(key, "false");
            }
            else
            {
                message.Add(Severity.Error, "ODATA query option {0} contains not allowed value {1}".FormatWith(ODinlineCount, value));
            }

            return true;
        }
        /// <summary>returns true if key.IsEqual($select). </summary>
        protected virtual bool ParseSelect(string key, IEnumerable<string> values
            , NameValueCollection localCollection, ServiceMessage message)
        {
            if (!key.IsEqual(ODselect))
            {
                return false; //Only proceed $select and its parameters here
            }

            foreach (var value in values.SelectMany(v => v.Split(',')))
            {
                var part = value.Trim();
                if (part.IsEmpty())
                {
                    message.Add(Severity.Error, "Wrong format of the ODATA string. Incorrect syntax after $select");
                    return true;
                }
                if (part.Contains("*"))
                {
                    message.Add(Severity.Info, "The symbol '*' (select all) is currently not supported. Incorrect syntax after $select");
                    return true;
                }
                localCollection.Add(key, part);
            }
            return true;
        }

        /// <summary>returns true if key.IsEqual($orderby). </summary>
        protected virtual bool ParseOrderBy(string key, IEnumerable<string> values
            , NameValueCollection localCollection, ServiceMessage message)
        {
            if (!key.IsEqual(ODorderby))
            {
                return false; //Only proceed $orderby and its parameters here
            }

            //just in case of $orderby, value should not be different value in string[], 
            // but should be represented as a comma separated string of values
            string unitedValue = string.Empty;

            foreach (var value in values.SelectMany(v => v.Split(',')))
            {
                var part = value.Trim();
                if (value.IsEmpty())
                {
                    message.Add(Severity.Error, "Wrong format of the ODATA string. Incorrect syntax after $orderby");
                    return true;
                }
                //If neither "asc" or "desc" are specified in the value, append it with " asc", as that is a default
                if (!OrderByParams.Any(s => value.ToLower().Contains(s)))
                {
                    part += " " + Ascending;
                }
                unitedValue += part + ",";
            }

            localCollection.Add(key, unitedValue.TrimEnd(','));
            return true;
        }
        #endregion parse OData Options

        #region protected
        protected virtual NameValueCollection OdataCollectionToInternalCollection(NameValueCollection odataCollection)
        {
            var collection = new NameValueCollection();

            foreach (var key in odataCollection.AllKeys)
            {
                string value = odataCollection[key];
                string[] values = odataCollection.GetValues(key);

                switch (key.ToLower())
                {
                    case ODtop:
                        collection.Add("TakeRows", value); //$top - might have just one value
                        break;
                    case ODskip:
                        collection.Add("SkipRows", value); //$skip - might have just one value
                        break;
                    case ODorderby:
                        collection.Add("OrderRows", value); //$orderby - might have just one value
                        break;
                    case ODinlineCount:
                        collection.Add("ShowInlineCount", value); //$inlinecount - might have just one value
                        break;
                    case ODselect:
                        AddValues(collection, "SelectColumns", values); //$select
                        break;
                    default:
                        AddFilterValues(collection, key, values); //$filter
                        break;
                }
            }

            return collection;
        }

        protected virtual void AddFilterValues(NameValueCollection collection, string key, string[] values)
        {
            if (key.IsEqual("ID"))
            {
                AddValues(collection, "IDColl", values);
                return;
            }

            if (key.EndsWith(".ID", StringComparison.InvariantCultureIgnoreCase))
            {
                AddValues(collection, key.Substring(0 , key.Length - 2) + "IDColl", values);
                return;
            }

            AddValues(collection, key, values);
        }

        protected virtual void AddValues(NameValueCollection target, string key, params string[] values)
        {
            foreach (string value in values)
            {
                target.Add(key, value);
            }
        }

        protected virtual void ResolveComparison(string[] preFinalSubstrings, int index, string delimitersPattern, NameValueCollection localCollection, ref ServiceMessage message)
        {
            string suffix = null;

            bool hasAnySign = Delimiters.Any((s => preFinalSubstrings[index].ToLower().Contains(s)));

            if (!hasAnySign)
            {
                message.Add(Severity.Error, "Wrong format of the ODATA string. Incorrect syntax after substring \"{0}\".".FormatWith(preFinalSubstrings[index]));
                return;
            }

            bool hasGreaterSign = GreaterDelimiters.Any((s => preFinalSubstrings[index].ToLower().Contains(s)));
            bool hasLesserSign = LesserDelimiters.Any((s => preFinalSubstrings[index].ToLower().Contains(s)));

            if (hasGreaterSign)
            {
                suffix = AppendFrom;
            }
            else if (hasLesserSign)
            {
                suffix = AppendTo;
            }

            string[] nameValue = Regex.Split(preFinalSubstrings[index], delimitersPattern, RegexOptions.IgnoreCase);

            string name = nameValue[0].Trim();
            string value = nameValue[1].Trim();

            localCollection.Add(name + suffix, value); //We are done with the substring by this
        }

        protected virtual IValueProvider ToProvider(NameValueCollection collection)
        {
            var pairs = new List<KeyValuePair<string, string>>();
            foreach (string key in collection.AllKeys)
            {
                foreach (string value in collection.GetValues(key))
                {
                    pairs.Add(new KeyValuePair<string, string>(key, value));
                }
            }

            var provider = new NameValuePairsValueProvider(pairs, CultureInfo.InvariantCulture);
            return provider;
        }
        #endregion protected
    }
}